/*
 * Copyright 1999-2018 Alibaba Group Holding Ltd.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.alibaba.nacos.core.listener;

import com.alibaba.nacos.api.exception.NacosException;
import com.alibaba.nacos.api.exception.runtime.NacosRuntimeException;
import com.alibaba.nacos.common.event.ServerConfigChangeEvent;
import com.alibaba.nacos.common.executor.ExecutorFactory;
import com.alibaba.nacos.common.executor.NameThreadFactory;
import com.alibaba.nacos.common.executor.ThreadPoolManager;
import com.alibaba.nacos.common.notify.NotifyCenter;
import com.alibaba.nacos.common.utils.StringUtils;
import com.alibaba.nacos.sys.env.EnvUtil;
import com.alibaba.nacos.sys.file.FileChangeEvent;
import com.alibaba.nacos.sys.file.FileWatcher;
import com.alibaba.nacos.sys.file.WatchFileCenter;
import com.alibaba.nacos.sys.utils.ApplicationUtils;
import com.alibaba.nacos.sys.utils.DiskUtils;
import com.alibaba.nacos.sys.utils.InetUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.boot.env.OriginTrackedMapPropertySource;
import org.springframework.context.ConfigurableApplicationContext;
import org.springframework.core.env.ConfigurableEnvironment;

import java.io.File;
import java.io.IOException;
import java.nio.file.Paths;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;

/**
 * init environment config.
 *
 * @author <a href="mailto:huangxiaoyu1018@gmail.com">hxy1991</a>
 * @since 0.5.0
 */
public class StartingApplicationListener implements NacosApplicationListener {

    private static final Logger LOGGER = LoggerFactory.getLogger(StartingApplicationListener.class);

    private static final String MODE_PROPERTY_KEY_STAND_MODE = "nacos.mode";

    private static final String MODE_PROPERTY_KEY_FUNCTION_MODE = "nacos.function.mode";

    private static final String LOCAL_IP_PROPERTY_KEY = "nacos.local.ip";

    private static final String NACOS_APPLICATION_CONF = "nacos_application_conf";

    private static final String NACOS_MODE_STAND_ALONE = "stand alone";

    private static final String NACOS_MODE_CLUSTER = "cluster";

    private static final String DEFAULT_FUNCTION_MODE = "All";

    private static final String DEFAULT_DATABASE = "mysql";

    /**
     * May be removed with the upgrade of springboot version.
     */
    public static final String DATASOURCE_PLATFORM_PROPERTY_OLD = "spring.datasource.platform";

    private static final String DATASOURCE_PLATFORM_PROPERTY = "spring.sql.init.platform";

    private static final String DERBY_DATABASE = "derby";

    private static final String DEFAULT_DATASOURCE_PLATFORM = "";

    private static final String DATASOURCE_MODE_EXTERNAL = "external";

    private static final String DATASOURCE_MODE_EMBEDDED = "embedded";
    // 保存配置文件资源信息
    private static final Map<String, Object> SOURCES = new ConcurrentHashMap<>();

    private ScheduledExecutorService scheduledExecutorService;

    private volatile boolean starting;

    // 下面的方法是从上到下一次执行的
    @Override
    public void starting() {
        starting = true;
    }

    @Override
    public void environmentPrepared(ConfigurableEnvironment environment) {
        // 创建工作目录
        makeWorkDir();
        // 注入环境变量 EnvUtil.setEnvironment(environment); 后续就可以通过 EnvUtil 获取环境变量中的值了
        injectEnvironment(environment);
        // 加载配置文件到环境中（这里优先加载自定义文件，如果没有配置自定义文件的话，就会加载默认的配置文件（/application.properties））
        loadPreProperties(environment);
        // 初始化系统变量，有两个：nacos.function.mode（All[默认为 All] | config | naming） 、nacos.mode (cluster 或者 stand alone)
        initSystemProperty();
    }

    @Override
    public void contextPrepared(ConfigurableApplicationContext context) {
        // 集群模式下，做一些日志打印 The server IP list of Nacos is xxx
        logClusterConf();
        // 一秒打印一次 Nacos is starting... 直到 starting = false
        logStarting();
    }

    @Override
    public void contextLoaded(ConfigurableApplicationContext context) {
        // 如果配置了 nacos.custom.environment.enabled = true
        // 那么会进去执行
        EnvUtil.customEnvironment();
    }

    @Override
    public void started(ConfigurableApplicationContext context) {
        // 成员变量改值，到这，上面哪个日志就不会打印了
        starting = false;
        // 还记得上面打印 starting ... 那个线程池吗，在这里给他关闭了
        closeExecutor();
        // 在标记 ApplicationUtils 中全局标记 started = true
        ApplicationUtils.setStarted(true);
        // 标记是否用了外部数据源
        judgeStorageMode(context.getEnvironment());
    }

    @Override
    public void failed(ConfigurableApplicationContext context, Throwable exception) {
        starting = false;

        makeWorkDir();

        LOGGER.error("Startup errors : ", exception);
        ThreadPoolManager.shutdown();
        WatchFileCenter.shutdown();
        NotifyCenter.shutdown();

        closeExecutor();

        context.close();

        LOGGER.error("Nacos failed to start, please see {} for more details.",
                Paths.get(EnvUtil.getNacosHome(), "logs/nacos.log"));
    }

    private void injectEnvironment(ConfigurableEnvironment environment) {
        // 这个 environment 是SpringBoot 回调给我们的 Spring上下文环境， 从这里我们直到，
        // 后续从 EnvUtil 这个对象中获取值，其实就是从 SpringBoot 的上下文环境中读数据
        EnvUtil.setEnvironment(environment);
    }

    /**
     * 加载配置文件
     *
     * @param environment Spring 上下文环境
     */
    private void loadPreProperties(ConfigurableEnvironment environment) {
        try {
            // 将配置文件中获取到的配置信息存入 SOURCES （一个 Map）中
            SOURCES.putAll(EnvUtil.loadProperties(EnvUtil.getApplicationConfFileResource()));
            // 将解析到的配置信息注入到 Spring 上下文环境中
            environment.getPropertySources()
                    // nacos_application_conf = SOURCES
                    .addLast(new OriginTrackedMapPropertySource(NACOS_APPLICATION_CONF, SOURCES));
            // 注册监听器
            registerWatcher();
        } catch (Exception e) {
            throw new NacosRuntimeException(NacosException.SERVER_ERROR, e);
        }
    }

    /**
     * 注册监听器
     *
     * @throws NacosException
     */
    private void registerWatcher() throws NacosException {
        // 监听 ${nacosHome}/conf 文件夹的变化
        WatchFileCenter.registerWatcher(EnvUtil.getConfPath(), new FileWatcher() {
            @Override
            public void onChange(FileChangeEvent event) {
                try {
                    // 一旦目标文件夹发生了数据变动，就会触发重新加载
                    Map<String, ?> tmp = EnvUtil.loadProperties(EnvUtil.getApplicationConfFileResource());
                    // 重新载入内存中
                    SOURCES.putAll(tmp);
                    // 发布一个 ｛ServerConfigChangeEvent｝ 事件
                    NotifyCenter.publishEvent(ServerConfigChangeEvent.newEvent());
                } catch (IOException ignore) {
                    LOGGER.warn("Failed to monitor file ", ignore);
                }
            }

            // 智力指定自己自己感兴趣的事件，这个代码表示我们只对 application.properties 的变化感兴趣
            // 也就是只有 application.properties 的改变才能触发上面的函数
            @Override
            public boolean interest(String context) {
                return StringUtils.contains(context, "application.properties");
            }
        });

    }

    /**
     * 初始化系统变量
     */
    private void initSystemProperty() {
        if (EnvUtil.getStandaloneMode()) {
            // 初始化 nacos.mode 变量，如果是单机模式，就设置变量为 stand alone （nacos.mode = stand alone）
            System.setProperty(MODE_PROPERTY_KEY_STAND_MODE, NACOS_MODE_STAND_ALONE);
        } else {
            // 如果不是单机模式，就设置为 nacos.mode = cluster
            System.setProperty(MODE_PROPERTY_KEY_STAND_MODE, NACOS_MODE_CLUSTER);
        }
        // 获取启动模式 可选值有 naming 和 config ， 默认 All 代表所有
        if (EnvUtil.getFunctionMode() == null) {
            // nacos.function.mode = All
            System.setProperty(MODE_PROPERTY_KEY_FUNCTION_MODE, DEFAULT_FUNCTION_MODE);
        } else if (EnvUtil.FUNCTION_MODE_CONFIG.equals(EnvUtil.getFunctionMode())) {
            // nacos.function.mode = config
            System.setProperty(MODE_PROPERTY_KEY_FUNCTION_MODE, EnvUtil.FUNCTION_MODE_CONFIG);
        } else if (EnvUtil.FUNCTION_MODE_NAMING.equals(EnvUtil.getFunctionMode())) {
            // nacos.function.mode = naming
            System.setProperty(MODE_PROPERTY_KEY_FUNCTION_MODE, EnvUtil.FUNCTION_MODE_NAMING);
        }
        // 记录本机ID :通过获取环境变量 nacos.local.ip 的值， 默认本机IP
        System.setProperty(LOCAL_IP_PROPERTY_KEY, InetUtils.getSelfIP());
    }

    /**
     * 如果是集群模式，这里打印一些日志，
     */
    private void logClusterConf() {
        if (!EnvUtil.getStandaloneMode()) {
            try {
                // 从 ${NacosHome}/conf/cluster.conf 中读取 IP 地址集合
                List<String> clusterConf = EnvUtil.readClusterConf();
                // 眼熟吗？这不就是钱启东的时候打印的第一行日志嘛。哈哈哈
                LOGGER.info("The server IP list of Nacos is {}", clusterConf);
            } catch (IOException e) {
                LOGGER.error("read cluster conf fail", e);
            }
        }
    }

    private void closeExecutor() {
        if (scheduledExecutorService != null) {
            scheduledExecutorService.shutdownNow();
        }
    }

    private void makeWorkDir() {
        String[] dirNames = new String[]{"logs", "conf", "data"};
        for (String dirName : dirNames) {
            LOGGER.info("Nacos Log files: {}", Paths.get(EnvUtil.getNacosHome(), dirName));
            try {
                // 获取 nacos 的工作路径 (不存在就创建)
                DiskUtils.forceMkdir(new File(Paths.get(EnvUtil.getNacosHome(), dirName).toUri()));
            } catch (Exception e) {
                throw new RuntimeException(e);
            }
        }
    }

    private void logStarting() {
        if (!EnvUtil.getStandaloneMode()) {
            // 启动一个调度线程池
            scheduledExecutorService = ExecutorFactory.newSingleScheduledExecutorService(
                    new NameThreadFactory("com.alibaba.nacos.core.nacos-starting"));
            // 一秒打印一次 Nacos is starting... 直到 starting = false
            scheduledExecutorService.scheduleWithFixedDelay(() -> {
                if (starting) {
                    LOGGER.info("Nacos is starting...");
                }
            }, 1, 1, TimeUnit.SECONDS);
        }
    }

    private void judgeStorageMode(ConfigurableEnvironment env) {

        // 依次从 spring.sql.init.platform 和 spring.datasource.platform 两个变量里面读取sql模式
        String platform = this.getDatasourcePlatform(env);
        // 是否使用了外部数据源 如果有值，且不是 derby 就代表使用了外部数据源
        boolean useExternalStorage =
                !DEFAULT_DATASOURCE_PLATFORM.equalsIgnoreCase(platform) && !DERBY_DATABASE.equalsIgnoreCase(platform);

        // must initialize after setUseExternalDB
        // This value is true in stand-alone mode and false in cluster mode
        // If this value is set to true in cluster mode, nacos's distributed storage engine is turned on
        // default value is depend on ${nacos.standalone}

        if (!useExternalStorage) {
            boolean embeddedStorage = EnvUtil.getStandaloneMode() || Boolean.getBoolean("embeddedStorage");
            // If the embedded data source storage is not turned on, it is automatically
            // upgraded to the external data source storage, as before
            if (!embeddedStorage) {
                useExternalStorage = true;
            }
        }

        LOGGER.info("Nacos started successfully in {} mode. use {} storage",
                System.getProperty(MODE_PROPERTY_KEY_STAND_MODE),
                useExternalStorage ? DATASOURCE_MODE_EXTERNAL : DATASOURCE_MODE_EMBEDDED);
    }

    /**
     * get datasource platform.
     *
     * @param env ConfigurableEnvironment.
     * @return
     */
    private String getDatasourcePlatform(ConfigurableEnvironment env) {
        // spring.sql.init.platform  默认为空
        String platform = env.getProperty(DATASOURCE_PLATFORM_PROPERTY, DEFAULT_DATASOURCE_PLATFORM);
        if (StringUtils.isBlank(platform)) {
            // 上面读不到，再读 spring.datasource.platform  默认也是空
            platform = env.getProperty(DATASOURCE_PLATFORM_PROPERTY_OLD, DEFAULT_DATASOURCE_PLATFORM);
        }
        return platform;
    }
}
